# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

from coin.exchange.base.kr_rest.private_client_base import PrivateClientBase
from coin.exchange.korbit_v1.kr_rest.product import KorbitProduct, KorbitCurrency

import coin.proto.coin_order_enums_pb2 as coin_order_pb2
from coin.exchange.korbit_v1.kr_rest.native_private_client import (KorbitNativePrivateClient,
                                                                   KorbitOrderStatus,
                                                                   KorbitAuthGrantType)
from coin.proto.coin_query_pb2 import (AccountBalance,
                                       CurrencyBalance,
                                       ProductFillElement,
                                       ProductFillBundle,
                                       ProductOrderElement,
                                       ProductOrderBundle)


def _native_symbol_from_open_order_response(msg):
  native_quote = msg['price']['currency']
  native_base = msg['total']['currency']
  native_symbol = '%s_%s' % (native_base, native_quote)
  return native_symbol


def _to_proto_order_side(korbit_order_side):
  if korbit_order_side == 'ask':
    order_side = coin_order_pb2.SELL_ORDER
  elif korbit_order_side == 'bid':
    order_side = coin_order_pb2.BUY_ORDER
  else:
    raise ValueError(korbit_order_side)
  return order_side


class KorbitPrivateParser(object):
  @staticmethod
  def parse_balance(json_msg):
    """
    [
      "xmr": {
        "available": "0",
        "trade_in_use": "0",
        "withdrawal_in_use": "0"
      },
      "xrp": {
        "available": "200",
        "trade_in_use": "0",
        "withdrawal_in_use": "0",
        "avg_price": "343.6",
        "avg_price_updated_at": 1556273700092
      },
      ....
    ]
    """
    currency_balance_list = []
    for currency, info in json_msg.items():
      hold = float(info['trade_in_use']) + float(info['withdrawal_in_use'])
      currency = KorbitCurrency.FromStrNativeCurrencyNothrow(currency)
      if currency is None:
        continue

      currency_balance = CurrencyBalance(currency=currency.currency,
                                         currency_native=currency.native_currency,
                                         available=float(info['available']),
                                         hold=hold,
                                         total=float(info['available']) + hold)
      currency_balance_list.append(currency_balance)

    return AccountBalance(exchange='Korbit', each_balance=currency_balance_list)

  @staticmethod
  def parse_fill(update_msg: list, product) -> ProductFillBundle:
    """
    [
      {
        "id": "89999",
        "currency_pair": "btc_krw",
        "side": "bid",
        "avg_price": "2900000",
        "price": "3000000",
        "order_amount": "0.81140000",
        "filled_amount": "0.33122200",
        "order_total": "2434200",
        "filled_total": "960543",
        "created_at": "1500033942638",
        "last_filled_at": "1500533946947",
        "status": "partially_filled",
        "fee": "0.00000500"
      },
      ....
    ]
    """
    currency_fill_list = []
    for update_elem in update_msg:
      if update_elem['currency_pair'] != product.native_symbol:
        continue
      if update_elem['status'] not in ['filled', 'partially_filled']:
        continue

      side = _to_proto_order_side(update_elem['type'])
      currency_fill = ProductFillElement(symbol=product.symbol,
                                         side=side,
                                         price=float(update_elem['price']),
                                         qty=float(update_elem['filled_amount']),
                                         fee=float(update_elem['fee']),
                                         fee_currency='KRW',
                                         order_id=update_elem['id'])
      currency_fill_list.append(currency_fill)

    return ProductFillBundle(exchange='Korbit', symbol=product.symbol, each_fill=currency_fill_list)

  @staticmethod
  def parse_orders(update_msg, product=None):
    """
    [
      {
        "timestamp": 1386405575000,
        "id": "36605",
        "type": "ask",
        "price": {
          "currency": "krw",
          "value": "1450000"
        },
        "total": {
          "currency": "btc",
          "value": "0.50000000"
        },
        "open": {
          "currency": "btc",
          "value": "0.50000000"
        }
      },
    ...
    ]
    """
    currency_order_list = []
    for update_elem in update_msg:
      native_symbol = _native_symbol_from_open_order_response(update_elem)
      update_product = KorbitProduct.FromStrNativeProduct(native_symbol)
      if product is not None and product != update_product:
        continue

      side = _to_proto_order_side(update_elem['type'])
      currency_order = ProductOrderElement(symbol=product.symbol,
                                           side=side,
                                           price=float(update_elem['price']['value']),
                                           qty=float(update_elem['total']['value']),
                                           order_id=str(update_elem['id']),
                                           created_timestamp=int(
                                               float(update_elem['timestamp']) * 1e+6))
      currency_order_list.append(currency_order)
    return ProductOrderBundle(exchange='Korbit',
                              symbol=product.symbol,
                              each_order=currency_order_list)


class KorbitPrivateClient(PrivateClientBase):
  ProductType = KorbitProduct

  def __init__(self, key_file):
    super().__init__()
    self.nprivc = KorbitNativePrivateClient(key_file=key_file)

  def update_token_if_needed(self):
    if not self.nprivc.has_token():
      update = self.nprivc.query_token(KorbitAuthGrantType.PASSWORD)
      self.nprivc.set_token(update.msg, update.res_timestamp)
    elif self.nprivc.need_refresh_token():
      update = self.nprivc.query_token(KorbitAuthGrantType.REFRESH_TOKEN)
      self.nprivc.set_token(update.msg, update.res_timestamp)
    else:
      pass

  def query_account_balance_impl(self):
    self.update_token_if_needed()
    update = self.nprivc.query_user_balances()
    update.msg = KorbitPrivateParser.parse_balance(update.msg)
    return update

  def query_fills_impl(self, product):
    self.update_token_if_needed()
    status_list = [KorbitOrderStatus.PARTIALLY_FILLED, KorbitOrderStatus.FILLED]
    symbol = product.native_symbol
    update = self.nprivc.query_user_orders(symbol, status_list)
    update.msg = KorbitPrivateParser.parse_fill(update.msg, product)
    return update

  def query_submit_impl(self, product, side, price, qty):
    self.update_token_if_needed()
    order_type = 'limit'
    fiat_amount = None
    currency_pair = product.native_symbol
    assert int(price) == price, price
    if side in ("buy", "bid"):
      update = self.nprivc.place_buy_order(currency_pair, order_type, int(price), qty, fiat_amount)
    elif side in ("sell", "ask"):
      update = self.nprivc.place_sell_order(currency_pair, order_type, int(price), qty)
    else:
      raise ValueError(side)
    return update

  # order_id can be a list of ids or a single id.
  def query_cancel_impl(self, product, order_id):
    self.update_token_if_needed()
    self.nprivc.cancel_orders(product.native_symbol, order_id)

  def query_cancel_product_impl(self, product):
    update_list = self.query_list_orders(product)
    for each_order in update_list.msg.each_order:
      self.query_cancel(product, each_order.order_id)

  def query_list_orders_impl(self, product):
    self.update_token_if_needed()
    currency_pair = product.native_symbol
    update = self.nprivc.query_open_orders(currency_pair=currency_pair)
    update.msg = KorbitPrivateParser.parse_orders(update.native_msg, product)
    return update

  def query_account_list_orders_impl(self):
    raise NotImplementedError()

  def query_account_fills_impl(self):
    raise NotImplementedError()

  def query_account_position_impl(self):
    raise NotImplementedError()
